let objectUtils = module.exports = {};


let hasOwnProperty = Object.prototype.hasOwnProperty;
let propIsEnumerable = Object.prototype.propertyIsEnumerable;

function toObject(val) {
    if (val === null || val === undefined) {
        throw new TypeError('Cannot convert undefined or null to object');
    }

    return Object(val);
}


function isObj(val) {
    return Object.prototype.toString.call(val) === '[object Object]';
}

objectUtils.isUndefined = function (val) {
    return Object.prototype.toString.call(val) === '[object Undefined]';
}

objectUtils.isNull = function (val) {
    return Object.prototype.toString.call(val) === '[object Null]';
};

function assignKey(to, from, key, copyAll) {
    let val = from[key];

    if (val === undefined || val === null) {
        return;
    }

    if (hasOwnProperty.call(to, key)) {
        if (to[key] === undefined || to[key] === null) {
            throw new TypeError('Cannot convert undefined or null to object (' + key + ')');
        }
    }

    if (!isObj(val)) {
        to[key] = val;
    } else {
        to[key] = assign(Object(to[key]), from[key], copyAll);
    }
}

function assign(to, from, copyAll) {
    if (to === from) {
        return to;
    }

    from = Object(from);

    for (let key in from) {

        if (objectUtils.isUndefined(from[key]) || objectUtils.isNull(from[key])) {
            if (!copyAll) {
                continue;
            }
        }

        if (hasOwnProperty.call(from, key)) {
            assignKey(to, from, key, copyAll);
        }
    }

    if (Object.getOwnPropertySymbols) {
        let symbols = Object.getOwnPropertySymbols(from);

        for (let i = 0; i < symbols.length; i++) {
            if (propIsEnumerable.call(from, symbols[i])) {
                assignKey(to, from, symbols[i], copyAll);
            }
        }
    }

    return to;
}

objectUtils.deepAssign = function deepAssign(target) {
    target = toObject(target);

    for (let s = 1; s < arguments.length; s++) {
        assign(target, arguments[s], true);
    }

    return target;
};


objectUtils.deepAssignIgnore = function deepAssign(target) {
    target = toObject(target);

    for (let s = 1; s < arguments.length; s++) {
        assign(target, arguments[s], false);
    }

    return target;
};
objectUtils.cmp = function (obj1, obj2) {
    if (objectUtils.isUndefined(obj1) && objectUtils.isUndefined(obj2)) {
        return true;
    }
    if (objectUtils.isNull(obj1) && objectUtils.isNull(obj2)) {
        return true;
    }
    if (objectUtils.isUndefined(obj1) != objectUtils.isUndefined(obj2)) {
        // one is undefined, the other is not
        return false;
    }
    if (objectUtils.isNull(obj1) != objectUtils.isNull(obj2)) {
        // one is null, the other is not
        return false;
    }
    if (objectUtils.isUndefined(obj1) && objectUtils.isNull(obj2)) {
        return false;
    }
    if (objectUtils.isNull(obj1) != objectUtils.isUndefined(obj2)) {
        return false;
    }
    //if both objects are numbers or strings or booleans, compare them directly with ===
    if (
        (
            typeof(obj1) === 'number' ||
            typeof(obj1) === 'string' ||
            typeof(obj1) === 'boolean'
        ) &&
        (
            typeof(obj2) === 'number' ||
            typeof(obj2) === 'string' ||
            typeof(obj2) === 'boolean'
        )
    ) {
        if (!(obj1 === obj2))
            return false;
        return true;
    }

    if (obj1.constructor === Object
        && obj2.constructor === Object) {
        //case 2 they are objects
        //in this case
        let keys1 = Object.keys(obj1);
        let keys2 = Object.keys(obj2);

        //if they have a different number of keys they are definetly different
        if (keys1.length != keys2.length)
            return false;

        //if the second object doesn't have all the keys the first one does -> !=
        let i;
        for (i = 0; i < keys1.length; ++i)
            if (!(obj2.hasOwnProperty(keys1[i]))) return false;

        //they have the same keys, so compare their keys's objects one by one
        for (i = 0; i < keys1.length; ++i)
            if (!objectCompare(obj1[keys1[i]], obj2[keys1[i]])) return false;

        //if theiy keys's objects are the same, return true
        return true;
    }

    if (obj1.constructor === Array
        && obj2.constructor === Array) {
        //if they don't have the same length, smth is wrong
        if (obj1.length != obj2.length) return false;

        //if the first array has all the elements found in the second one and vice versa they are the same (although the complexity of the algorythm is kinda big)
        let foundIn1 = 0, foundIn2 = 0;
        let i, j;
        for (i = 0; i < obj1.length; ++i)
            for (j = 0; j < obj2.length; ++j)
                if (objectCompare(obj1[i], obj2[j])) {
                    ++foundIn2;
                    break;
                }

        for (j = 0; j < obj2.length; ++j)
            for (i = 0; i < obj1.length; ++i)
                if (objectCompare(obj2[j], obj1[i])) {
                    ++foundIn1;
                    break;
                }

        if (foundIn1 === foundIn2 && foundIn1 === obj1.length)
            return true;
    }
    return false;
};

objectUtils.cmpObjectProperty = function (obj1, obj2, ...properties) {
    //if both objects are numbers or strings or booleans, compare them directly with ===
    if (
        (
            typeof(obj1) === 'number' ||
            typeof(obj1) === 'string' ||
            typeof(obj1) === 'boolean'
        ) &&
        (
            typeof(obj2) === 'number' ||
            typeof(obj2) === 'string' ||
            typeof(obj2) === 'boolean'
        )
    ) {
        if (!(obj1 === obj2))
            return false;
        return true;
    }

    for (let i = 0; i < properties.length; i++) {
        if (!objectUtils.cmp(obj1[properties[i]], obj2[properties[i]])) {
            return false
        }
    }
    return true;
};